/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.inspur.edp.lcm.metadata.core;

import com.inspur.edp.lcm.metadata.api.entity.ActionContext;
import com.inspur.edp.lcm.metadata.api.entity.DbConnectionInfo;
import com.inspur.edp.lcm.metadata.api.entity.DebugEnvironment;
import com.inspur.edp.lcm.metadata.api.entity.DeploymentContext;
import com.inspur.edp.lcm.metadata.api.entity.ExtractContext;
import com.inspur.edp.lcm.metadata.api.entity.GspProject;
import com.inspur.edp.lcm.metadata.api.entity.MetadataCompilerContext;
import com.inspur.edp.lcm.metadata.api.entity.compiler.CompilerConfiguration;
import com.inspur.edp.lcm.metadata.api.entity.extract.ExtractConfigration;
import com.inspur.edp.lcm.metadata.common.FileServiceImp;
import com.inspur.edp.lcm.metadata.common.StageExecutorService;
import com.inspur.edp.lcm.metadata.common.Utils;
import com.inspur.edp.lcm.metadata.common.compiler.MetadataCompilerHelper;
import com.inspur.edp.lcm.metadata.common.deployer.DeploymentHelper;
import com.inspur.edp.lcm.metadata.common.extractor.ExtractHelper;
import com.inspur.edp.lcm.metadata.common.util.CommandUtil;
import com.inspur.edp.lcm.metadata.common.util.process.ProcessUtilFactory;
import com.inspur.edp.lcm.metadata.core.thread.ExtractTask;
import com.inspur.edp.lcm.metadata.core.thread.MockWebProcessTask;
import com.inspur.edp.lcm.metadata.core.thread.ProcessRunnable;
import com.inspur.edp.lcm.metadata.inner.api.data.PublishStatus;
import com.inspur.edp.lcm.metadata.inner.api.enm.StageEnum;
import com.inspur.edp.lcm.metadata.inner.api.utils.IdeLogUtils;
import com.inspur.edp.lcm.metadata.spi.AfterDeployAction;
import com.inspur.edp.lcm.metadata.spi.ExtractAction;
import com.inspur.edp.lcm.metadata.spi.MetadataCompileAction;
import com.inspur.edp.lcm.project.deployer.api.DeployService;
import com.inspur.edp.lcm.project.deployer.spi.DeployAction;
import io.iec.edp.caf.boot.context.CAFContext;
import io.iec.edp.caf.common.environment.EnvironmentUtil;
import io.iec.edp.caf.commons.utils.SpringBeanUtils;
import io.iec.edp.caf.sumgr.core.ServiceUnitFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import sun.misc.BASE64Decoder;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @author zhaoleitr
 */
@Slf4j
public class ProjectExtendCoreService {
    private final FileServiceImp fileService = new FileServiceImp();
    private final MetadataProjectCoreService metadataProjectCoreService = new MetadataProjectCoreService();
    private final GspProjectCoreService gspProjectCoreService = new GspProjectCoreService();
    private DeployService deployService = SpringBeanUtils.getBean(DeployService.class);

    public String getDeployStatus(String sign) throws IOException {
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] bytes = decoder.decodeBuffer(sign);
        String urlEncodeSign = new String(bytes);
        String fileName = URLDecoder.decode(urlEncodeSign, "utf8");
        String status = "";
        if (fileService.isFileExist(fileName)) {
            status = fileService.fileRead(fileName);
            switch (status) {
                case "success":
                case "fail":
                    fileService.fileDelete(fileName);
                    break;
                default:
                    break;
            }
        }
        return status;
    }

    public void extract(String absolutePath) {
        List<String> projPaths = new ArrayList<>();
        if (absolutePath.endsWith(Utils.getMetadataProjPath())) {
            absolutePath = new File(absolutePath).getParent();
        }
        metadataProjectCoreService.getProjPathsInPath(absolutePath, projPaths);
        CountDownLatch latch = new CountDownLatch(projPaths.size());
        List<CompletableFuture<Boolean>> extractResultList = new ArrayList<>();
        for (String projPath : projPaths) {
            GspProject gspProjectInfo = gspProjectCoreService.getGspProjectInfo(projPath);
            ExtractContext extractContext = new ExtractContext();
            extractContext.setProjectPath(projPath);
            extractContext.setDeployPath(gspProjectInfo.getSuDeploymentPath());
            // 异步执行提取过程
            CompletableFuture<Boolean> cf = CompletableFuture.supplyAsync(ExtractTask.ofExtractTask(IdeLogUtils.getRequestId(), extractContext, latch, CAFContext.current.getCurrentSession()), StageExecutorService.getStageExecutorService())
                    // 非阻塞同步回调方法
                    .thenApply((re) -> {
                        IdeLogUtils.syncProcess(PublishStatus.success(IdeLogUtils.calcProcess(projPaths.size(), (int) (projPaths.size() - latch.getCount()), StageEnum.EXTRACT), StageEnum.EXTRACT.getCode()));
                        return true;
                    });
            extractResultList.add(cf);
        }
        // 等待所有成功结束
        CompletableFuture<Void> cfAll = CompletableFuture.allOf(extractResultList.toArray(new CompletableFuture[extractResultList.size()]));
        try {
            cfAll.get();
        } catch (Exception e) {
            log.error("提取过程异常:", e);
            IdeLogUtils.syncProcess(PublishStatus.fail("工程提取发生异常: " + e.toString() + " 具体错误详情请查看系统日志。"));
            IdeLogUtils.setStageStatus(IdeLogUtils.Fail);
            return;
        }
    }

    public void doExtract(ExtractContext extractContext) {
        // 文件清理
        clearPublishDir(extractContext);
        // 组织上下文
        buildExtractContext(extractContext);
        // 各类型提取扩展
        List<ExtractConfigration> extractConfigrationList = ExtractHelper.getInstance().getExtractConfigrationList();
        extractConfigrationList.forEach(item -> {
            if (item.isEnable()) {
                if (extractContext.getDeployType() != null && ("WEB").equals(item.getTypeCode()) && !"all".equals(extractContext.getDeployType()) && !extractContext.getDeployType().contains("web")) {
                    return;
                }
                ExtractAction extractAction = ExtractHelper.getInstance().getManager(item.getTypeCode());
                if (extractAction == null) {
                    throw new RuntimeException("未能获取到" + item.getTypeCode() + "的提取器，请确认配置是否正确");
                }
                extractAction.extract(extractContext);
            }
        });
    }

    private void buildExtractContext(ExtractContext context) {
        String publishPath = Paths.get(context.getProjectPath()).resolve(Utils.getPublishPath()).toString();
        String deployPath = Paths.get(publishPath).resolve(Utils.getServerPath()).resolve(context.getDeployPath().toLowerCase()).toString();
        context.setDeployPath(deployPath);
    }

    private void clearPublishDir(ExtractContext extractContext) {
        String publishPath = Paths.get(extractContext.getProjectPath()).resolve(Utils.getPublishPath()).toString();
        File file = new File(publishPath);
        if (!file.exists()) {
            file.mkdir();
        }
        try {
            FileServiceImp.cleanDirectory(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void deploy(DbConnectionInfo dBConnectionInfo, String serverPath, String projPath, String restart) {
        serverPath = serverPath == null || serverPath.isEmpty() ? Utils.getBasePath() : serverPath;

        beforeDeploy(serverPath, projPath);

        String publishPath = Paths.get(metadataProjectCoreService.getProjPath(projPath)).resolve(Utils.getPublishPath()).toString();
        if ("0".equals(restart)) {
            reStart(dBConnectionInfo, serverPath, restart, publishPath);
        } else {
            // 不重启就不启进程 ProcessUtilFactory.getProcessUtil().execGracefully(command, currentServerPath);
            deployService.deploy(buildDeploymentContext(publishPath, serverPath, dBConnectionInfo));
            // 部署后扩展
            Map<String, AfterDeployAction> actionMap = SpringBeanUtils.getApplicationContext().getBeansOfType(AfterDeployAction.class);
            if (!CollectionUtils.isEmpty(actionMap.values())) {
                ActionContext actionContext = new ActionContext(projPath);
                for (AfterDeployAction action : actionMap.values()) {
                    action.hotLoad(actionContext);
                }
            }
        }
    }

    public void reStart(DbConnectionInfo dBConnectionInfo, String serverPath, String restart, String publishPath) {
        String args = handleArgs(dBConnectionInfo, serverPath, publishPath, restart);
        String currentServerPath = Paths.get(Utils.getBasePath()).resolve("tools/deploy/project_java/runtime").toString();
        String javaHome = ProcessUtilFactory.getProcessUtil().getJavaHome();
        String javaCmd = javaHome == null ? "java" : javaHome + "/bin/java";
        String serverRuntimePathName = EnvironmentUtil.getServerRuntimePathName();
        String command = javaCmd + " -server -Dloader.path=libs,3rd -Dserver.runtime.path.name=" + serverRuntimePathName + " -Dspring.config.location=../config/application.yaml -jar lcm-project-deploy-tool.jar" + args;
        newProcessRunnable(currentServerPath, command);
    }

    private DeploymentContext buildDeploymentContext(String publishPath, String serverPath, DbConnectionInfo dbInfo) {
        DeploymentContext deploymentContext = new DeploymentContext();
        deploymentContext.setPublishPath(publishPath);
        deploymentContext.setCallType(DeploymentContext.CallTypeFromService);
        DebugEnvironment debugEnvironment = new DebugEnvironment();
        debugEnvironment.setServerPath(serverPath);
        debugEnvironment.setLanguage("_CHS");
        debugEnvironment.setDeployPath("");

        debugEnvironment.setDbConnectionInfo(dbInfo);
        debugEnvironment.setCentralDbConnectionInfo(dbInfo);

        deploymentContext.setDebugEnvironment(debugEnvironment);

        return deploymentContext;
    }

    private void beforeDeploy(String serverPath, String projPath) {
        if (!suFileExists(serverPath, projPath)) {
            generateSuFile(serverPath, projPath);
        }
    }

    private boolean suFileExists(String serverPath, String projPath) {
        GspProject gspProjectInfo = gspProjectCoreService.getGspProjectInfo(projPath);
        String serverLink = new File(FileServiceImp.combinePath(serverPath, "jstack")).exists() ? "jstack" : "server";
        String destDir = FileServiceImp.combinePath(serverPath, serverLink, "apps", gspProjectInfo.getAppCode().toLowerCase(), gspProjectInfo.getServiceUnitCode().toLowerCase());
        File file = new File(destDir);
        file.mkdirs();
        return ServiceUnitFactory.createGenerator().exists(file);
    }

    private void generateSuFile(String serverPath, String projPath) {
        GspProject gspProjectInfo = gspProjectCoreService.getGspProjectInfo(projPath);
        io.iec.edp.caf.sumgr.api.entity.ServiceUnitInfo serviceUnitInfo = new io.iec.edp.caf.sumgr.api.entity.ServiceUnitInfo();
        serviceUnitInfo.setApplicationName(gspProjectInfo.getAppCode());
        serviceUnitInfo.setName(gspProjectInfo.getServiceUnitCode());
        String serverLink = new File(FileServiceImp.combinePath(serverPath, "jstack")).exists() ? "jstack" : "server";
        String destDir = FileServiceImp.combinePath(serverPath, serverLink, "apps", gspProjectInfo.getAppCode().toLowerCase(), gspProjectInfo.getServiceUnitCode().toLowerCase());
        File file = new File(destDir);
        ServiceUnitFactory.createGenerator().generate(serviceUnitInfo, file);
    }

    private void newProcessRunnable(String serverPath, String command) {
        ProcessRunnable processRunnable = new ProcessRunnable();
        processRunnable.setServerPath(serverPath);
        processRunnable.setCommand(command);
        ExecutorService executor = new ThreadPoolExecutor(0, 64,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<>(),
                new ThreadFactory() {
                    private final AtomicInteger integer = new AtomicInteger(0);
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r);
                        thread.setName("lcm-deploy-and-restart-" + integer.incrementAndGet());
                        return thread;
                    }
                });
        executor.submit(processRunnable::run);
    }

    private String handleArgs(DbConnectionInfo dbConnectionInfo, String serverPath, String publishPath, String restart) {
        StringBuilder stringBuilder = new StringBuilder();
        appendArg(stringBuilder, publishPath);
        appendArg(stringBuilder, serverPath);
        appendArg(stringBuilder, dbConnectionInfo.getDbServerIP());
        appendArg(stringBuilder, dbConnectionInfo.getDbPort());
        appendArg(stringBuilder, dbConnectionInfo.getDbName());
        appendArg(stringBuilder, dbConnectionInfo.getDbUserName());
        appendArg(stringBuilder, dbConnectionInfo.getDbPassword());
        appendArg(stringBuilder, String.valueOf(dbConnectionInfo.getDbType().ordinal()));
        appendArg(stringBuilder, restart);

        if (restart.equals("0")) {
            int currentPid = CommandUtil.getCurrentPid();
            appendArg(stringBuilder, String.valueOf(currentPid));
        }

        return stringBuilder.toString();
    }

    private void appendArg(StringBuilder stringBuilder, String arg) {
        if (Utils.isNullOrEmpty(arg) || arg.contains(" ")) {
            stringBuilder.append(" \"");
            stringBuilder.append("\"");
        } else {
            stringBuilder.append(" ");
            stringBuilder.append(arg);
        }
    }

    public void migration(String absolutePath) {
        DeploymentContext context = new DeploymentContext();
        String publishPath = Paths.get(metadataProjectCoreService.getProjPath(absolutePath)).resolve(Utils.getPublishPath()).toString();
        context.setPublishPath(publishPath);
        DebugEnvironment debugEnvironment = new DebugEnvironment();
        debugEnvironment.setServerPath(Utils.getBasePath());
        context.setDebugEnvironment(debugEnvironment);
        DeployAction baseMigration = DeploymentHelper.getInstance().getManager("BaseMigration");
        if (baseMigration == null) {
            throw new RuntimeException("未能获取到BaseMigration的部署器，请确认配置是否正确");
        }
        baseMigration.deploy(context);
    }

    public List<String> batchCompile(String absolutePath, String exts, String disabledExts, String packagePath) {
        List<String> projPaths = new ArrayList<>();
        if (absolutePath.contains(",")) {
            projPaths = Arrays.asList(absolutePath.split(","));
        } else {
            if (absolutePath.endsWith(Utils.getMetadataProjPath())) {
                absolutePath = new File(absolutePath).getParent();
            }
            metadataProjectCoreService.getProjPathsInPath(absolutePath, projPaths);
        }

        List<String> extsList = exts.isEmpty() ? new ArrayList<>() : Arrays.asList(exts.split(","));
        List<String> disabledExtsList = disabledExts.isEmpty() ? new ArrayList<>() : Arrays.asList(disabledExts.split(","));
        int count = 0;
        for (String projPath : projPaths) {
            MockWebProcessTask task = null;
            try {
                count++;
                log.info("正在编译：" + projPath);
                String projectMetadataPath = Paths.get(projPath).resolve(Utils.getMetadataProjPath()).toString();
                MetadataCompilerContext metadataCompilerContext = new MetadataCompilerContext();
                metadataCompilerContext.setProjectPath(projectMetadataPath);
                // 上一个工程执行进度
                int lastProcess = IdeLogUtils.calcProcess(projPaths.size(), count - 1, StageEnum.EXTEND_COMPILE);
                // 当前工程执行进度
                int currentProcess = IdeLogUtils.calcProcess(projPaths.size(), count, StageEnum.EXTEND_COMPILE);
                task = MockWebProcessTask.ofMockWebProcessTask(IdeLogUtils.getRequestId(), lastProcess, currentProcess);
                Thread mockWebProcessThread = new Thread(task);
                mockWebProcessThread.start();
                compile(metadataCompilerContext, extsList, disabledExtsList);
                log.info("编译完成：" + projPath);
                task.setExit(true);
                IdeLogUtils.syncProcess(
                        PublishStatus.success(currentProcess, StageEnum.EXTEND_COMPILE.getCode())
                );
                Thread.sleep(Utils.PUBLISH_WAIT_TIME);
            } catch (InterruptedException e) {
                continue;
            } catch (Exception e) {
                if (Objects.nonNull(task)) {
                    task.setExit(true);
                }
                IdeLogUtils.setStageStatus(IdeLogUtils.Fail);
                IdeLogUtils.syncProcess(PublishStatus.fail(projPath + "工程扩展编译发生异常: " + e.toString() + " 具体错误详情请查看系统日志。"));
                throw e;
            }
            // 如果有工程执行报错，则终止发布流程
            if (!IdeLogUtils.SUCCESS.equals(IdeLogUtils.getStageStatus())) {
                return null;
            }
        }

        return null;
    }

    /**
     * 模拟前端编译进度
     *
     * @return
     */
    private Thread mockWebProcess() {
        return new Thread() {
            /**
             * 退出标识位
             */
            private volatile boolean exit = false;

            @Override
            public void run() {
                while (!exit) {
                    try {
                        Thread.sleep(500);
                        if (!exit) {
                            IdeLogUtils.syncProcess(
                                    PublishStatus.success(70, StageEnum.EXTEND_COMPILE.getCode())
                            );
                        }
                    } catch (InterruptedException e) {
                        log.warn("前端编译实时发送进度过程发生等待中断异常:", e);
                    }
                }
            }

            public void setExit(boolean exit) {
                this.exit = exit;
            }
        };
    }

    private void compile(MetadataCompilerContext metadataCompilerContext, List<String> extsList, List<String> disabledExtsList) {
        List<CompilerConfiguration> compileConfigrationList = MetadataCompilerHelper.getInstance().getCompileConfigrationList();
        if (extsList != null && extsList.size() > 0) {
            compileConfigrationList = compileConfigrationList.stream().filter(config -> extsList.contains(config.getTypeCode())).collect(Collectors.toList());
        }
        if (disabledExtsList != null && disabledExtsList.size() > 0) {
            compileConfigrationList = compileConfigrationList.stream().filter(config -> !disabledExtsList.contains(config.getTypeCode())).collect(Collectors.toList());
        }
        for (CompilerConfiguration item : compileConfigrationList) {
            if (item.isEnable()) {
                MetadataCompileAction manager = MetadataCompilerHelper.getInstance().getMetadataCompiler(item.getTypeCode());
                if (manager != null) {
                    manager.metadataCompile(metadataCompilerContext);
                }
            }
        }
    }
}
